#!/usr/bin/perl

## (C) Copyright 2010- Hewlett-Packard Development Company, L.P.
##
## $Id: sqconfig_logical_to_physical.pl 17769 2012-04-27 07:14:44Z yi.wang4@hp.com $

use strict;
use warnings;

use Storable qw( store_fd store );
use Data::Dumper qw( Dumper );
use Getopt::Long;
use POSIX;
use XML::Twig;
use File::Basename;
use Cwd 'abs_path';
use List::Util qw(max);

# perl module containing log handling routines
# and instance processing routines
use lib '/opt/hp/nv/lib/perl';
use config ':all';
use instance ':all';

# possible error returns
my $OK = 0;
my $WARN = 1;
my $CRITICAL = 2;
my $UNKNOWN = 3;

use constant DBMAX => 16;
use constant AUMAX => 4;

my $myscriptname = "sqconfig_logical_to_physical";
my $revision = '$Revision: 17769 $';
my $csvfile = "/home/lunmgr/nodemap.csv";
# Status file for script
my $mystatusname = "$myscriptname.stat";
my $status_file = "$nvpath/instance/status/" . $mystatusname;

# globals used in this script
my $config = "";

# Map logical node id to physical node name and roles
#
# Structure of %gh_logical_nodes:
#
#       <logical-node-id> {
#           'node-name',  # Physical node name, e.g. 'n1'
#           'node-roles', # Node roles, as specified in sqconfig
#           'rack'        # Physical rack the node is located in
#       };
my %gh_logical_nodes;

# Map SeaQuest logical volume name to various worthwhile attributes
#
# Structure of %gh_sqvolumes:
#
#       $<VOLUME_NAME> {
#           'node-id',   # Logical node id for this volume
#           'node-name', # Physical node name for this volume
#           'rack'       # Physical rack the volume is located in
#           'prim-loc',  # Location to mount the primary LUN
#           'prim-wwn',  # WWN of the controller for the primary LUN
#           'prim-lun',  # LUN number for the primary LUN
#           'mir-loc',   # Location to mount the mirror LUN
#           'mir-wwn',   # WWN of the contorller for mirror LUN
#           'mir-lun'    # LUN number for the mirror LUN
#       };
my %gh_sqvolumes = ();

# Map a FC switch IP to the number of disk enclosures attached
# Use: my $num_encs = $gh_numenc_per_switch{$ip}
my %gh_numenc_per_switch = ();

# my @ga_lun_pool = ();           # Pool of LUN hashes, grouped together by disk enclosure
my %gh_luns_seen = ();            # List of LUN UUIDs we've already seen
my $gi_max_numenc_per_switch = 0; # Maximum number of disk enclosures per switch (in case not all switches are the same)
my $gi_name_free_luns = 0;        # Assign logical names to remaining "FREE" luns after distributing SQ logical names

use constant DASHES => "----------------------------------------------------------------------";

##############################################################################
##############################################################################
## This sub calls module routine to log informational message to log file
## after first prepending program name; timestamp will be added in
## Perl module
sub log_msg
{
    my $message =  shift;
    my $infomessage;

    $infomessage = $myscriptname . ": INFO: ". $message . "\n";

    instance_log_event($infomessage);
}

## This sub calls module routine to log warning message to log file
## after first prepending program name and Warning label; a timestamp 
## will be added in Perl module
sub log_warn
{
    my $message =  shift;
    my $warnmessage;

    $warnmessage = $myscriptname . ": WARNING: ". $message . "\n";

    instance_log_event($warnmessage);
}

## This sub calls module routine to log error message to log file
## after first prepending program name; timestamp will be added in
## Perl module; the program will then exit
sub logerr_n_die
{
    my $message =  shift;
    my ($errormessage, $stderrmessage);

    $errormessage = $myscriptname . ": ERROR: ". $message . "\n";

    instance_log_event($errormessage);
    print STDERR $errormessage;

    exit($CRITICAL);
}

##############################################################################
##############################################################################

## This sub sets the script status to be examined
## the next time the script is run.
## The status file could just hold "completed", for example,
## or it could hold a count of how many things were processed last time,
## e.g. number of nodes, or number of disks, etc.  So that a comparison
## can be made to see if processing must be done again.
sub set_status
{
    my $statusmsg = shift;

#    return if ($dryrun); # Don't overwrite or set status in this case

    # Keep the open and close of the file
    open(OFH, ">>$status_file") or logerr_n_die("Can't open file $status_file: $!");

    print(OFH "$statusmsg\n");

    close(OFH);
}
sub main
{
    my ($Ssqconfig_file, $Slunmgr_xml_file);
    my $errmsg = "Must specify %s location!\nFor example, \"%s --sqconfig [loc] --lmconfig [loc]\"\n";

    my $getOptRes = GetOptions
    (
        'sqconfig|s=s'     => \$Ssqconfig_file,
        'lmconfig|l=s'     => \$Slunmgr_xml_file,
        'name_free_luns|n' => \$gi_name_free_luns
    );

    $revision =~ s/\$//g;
    log_msg("Starting script: revision number $revision");
    set_status("starting");

    logerr_n_die(sprintf($errmsg, "sqconfig", basename($0))) if (!$Ssqconfig_file && !$gi_name_free_luns);
    logerr_n_die(sprintf($errmsg, "lunmgr XML", basename($0))) if (!$Slunmgr_xml_file);

    chomp(my $lmconfig_type = `file $Slunmgr_xml_file`);

    logerr_n_die("Error: $Slunmgr_xml_file is not a valid XML file") if ($lmconfig_type !~ /XML/);

#     print "Debug: sqconfig: \"$Ssqconfig_file\"  lunmgr XML: \"$Slunmgr_xml_file\"\n\n";

    # get the instance configuration type, whether sliced or shared
    $config = get_inst_config_type();
    if (!$config) {
	logerr_n_die("The instance config type is undefined");
    }
    elsif($config !~ /^(shared|sliced)$/) {
	logerr_n_die("The instance config type \"$config\" is invalid, neither \"shared\" nor \"sliced\"");
    }

    set_status("ParseSqconfig");
    ParseSqconfig($Ssqconfig_file) if (defined($Ssqconfig_file));

    set_status("ParseLunmgrXML");
    my $twig = ParseLunmgrXML($Slunmgr_xml_file);

    set_status("DistributeLUNs");
    DistributeLUNs($twig, $Slunmgr_xml_file);

    log_msg("$myscriptname ending");
    set_status("completed");

}

sub ParseSqconfig($)
{
    my $sqconfig = shift;

    log_msg("Parsing sqconfig file $sqconfig");

    sysopen(SQCONFIG, $sqconfig, O_RDONLY) ||
        logerr_n_die("Error opening $sqconfig: $!");

    # Parse sqconfig
    while (<SQCONFIG>)
    {
        ParseNodes() if (/^begin node/); # Handle nodes section
        ParseVolumes() if (/^begin [tma]+se/)
    }

    close(SQCONFIG);

#     print "Debug:\n" . Dumper(%gh_sqvolumes) . "\n";
}

##
## Parse through the nodes section in the sqconfig, populating the
## the %gh_logical_nodes hash.
##
sub ParseNodes()
{
    my ($nid, $nname, $ncores, $nprocs, $nroles);
    my (@line, @fields);
    my $xcflag = 1; # Assume XC to begin with

    $xcflag = 0 if (! -e "/etc/hptc-release"); # Not an XC cluster

    while (<SQCONFIG>)
    {
        # Finish up
        if(/^end node/)
        {
            for $nid (keys %gh_logical_nodes) {
                my $node = $gh_logical_nodes{$nid}->{'node-name'};
                $gh_logical_nodes{$nid}{rack} = get_node_rack($node);
            }
            return;
        }

        next if (/^#/ || /^\s*$/); # Comment or empty lines

        chomp(@line = split(";", $_));

        # Get logical node id, 0-n
        @fields = split("=", $line[0]);
        $nid = $fields[-1];

        # Get physical node name (e.g. n1)
        @fields = split("=", $line[1]);
        $nname = $fields[-1];
        $nname =~ s/ $//; # Trim trailing space

        # Get roles (probably not needed)
        @fields = split("=", $line[-1]);
        $nroles = $fields[-1];

        $gh_logical_nodes{$nid}{'node-name'}  = $nname;
        $gh_logical_nodes{$nid}{'node-roles'} = $nroles;
    }

    # Shouldn't get here - badly formed sqconfig (missing end nodes)
    logerr_n_die("Syntax error in sqconfig, processing cannot continue");

}

##
## Parse through the various SQ volume sections in the sqconfig, and populate
## the %gh_sqvolumes hash.
##
sub ParseVolumes()
{
    my ($volname, $volnid, $volprim, $volmir);
    my @line;
    my @dirparts;
    my @dirpartsmir;

    while (<SQCONFIG>)
    {
        return if (/^end [tma]+se/);
        next if (/^#/ || /^\s*$/); # Comment or empty lines

        chomp(@line = split(/\s+/, $_));

        $volname = $line[0];
        $volnid  = $line[1];
        $volprim = $line[2];
        $volmir  = $line[3];

	# If shared configuration we need to change the volume name as well as
	# the primary and mirror volumes -- these will reflect what is needed
	# for a shared configuration as compared to a sliced configuration
	if ($config eq "shared") {
		chomp(@dirparts = split(/\//, $volprim));
		$volname = "$volname-$dirparts[2]";
		$volprim = "/" . $dirparts[1];
		chomp(@dirpartsmir = split(/\//, $volmir));
		$volmir = "/" . $dirpartsmir[1];
	}

        $gh_sqvolumes{$volname}{'node-id'}   = $volnid;
        $gh_sqvolumes{$volname}{'prim-loc'}  = $volprim;
        $gh_sqvolumes{$volname}{'mir-loc'}   = $volmir;
        $gh_sqvolumes{$volname}{'node-name'} = $gh_logical_nodes{$volnid}{'node-name'};
        $gh_sqvolumes{$volname}{'rack'} = $gh_logical_nodes{$volnid}{'rack'};
    }

    # Shouldn't get here - badly formed sqconfig (missing end [tma]se)
    logerr_n_die("Syntax error in sqconfig (missing end [tma]se), processing cannot continue");
}

sub ParseLunmgrXML($)
{
    my $lunmgr_config = shift;
    my ($twig, $twig_root, $wwn, $racknum);
    my ($device, $devmpath, $devsize, $devmnt, $devname);
    my $numluns = 0; # Debugging

    log_msg("Parsing lunmgr xml file $lunmgr_config");

    $twig = new XML::Twig(
        twig_roots => {'rack' => 1}, # Throw away the switch information, it isn't too important
        pretty_print => 'indented',
        empty_tags => 'html'
    );

    $twig->parsefile($lunmgr_config);
    $twig_root = $twig->root;

    # Loop through the racks
    foreach my $rack ($twig_root->children)
    {
        $racknum = $rack->att('number');
        # Validate racknum to make sure it is configured.
        logerr_n_die("Failed to load rack number; need to check lunmgr config file.") unless length($racknum)>0;

        # Loop through the FC HBA ports
        foreach my $hbaport ($rack->children())
        {
            my $port = $hbaport->att('wwpn');
            $gh_numenc_per_switch{$port} = 0;

            foreach my $diskenc ($hbaport->children())
            {
                my %Hlun = ();
                $wwn = $diskenc->att('wwn'); # Get the WWN of this disk enclosure

                foreach my $lun ($diskenc->children)
                {
                    my $lunnum = $lun->att('id');
                    ($device, $devmpath, $devsize, $devmnt, $devname) = $lun->children_text;

                    $gh_luns_seen{$device} = "seen" if (!defined($gh_luns_seen{$device}));
                    $gh_luns_seen{$device} = "used" if (uc($devname) ne "FREE"); # Previously configured
                    $Hlun{$device}{'lun-id'} = $lunnum;
                    $Hlun{$device}{'wwn'} = $wwn;
                    $Hlun{$device}{'rack'} = $racknum;

                    $numluns++;
                }

#                 push(@ga_lun_pool, \%Hlun); # Push associated LUNs into the pool
                $gh_numenc_per_switch{$port}++;
            }
        }
    }

    $gi_max_numenc_per_switch = max(values(%gh_numenc_per_switch));

    return ($twig);
}

##
## Loop through lunmgr XML configuration file, and assign SQ volumes to
## LUNs.  For each volume, given primary disk assigned to a LUN in
## enclosure X, the mirror disk is assigned to enclosure X + 1.  When the
## last enclosure is reached on a given switch, we loop back around to the
## first disk enclosure.
##
## Parameters:
##      [1] - Reference to the lunmgr XML Twig
##      [2] - Filename for the lunmgr XML configuration
##      [3] - Option to give remaining FREE LUNs a name of the form R#B#L#
##
sub DistributeLUNs($$$)
{
    my ($lunmgr_twig, $lunmgr_config, $namefree) = @_;
    my $OUT_FMT = "%-13.13s %-7.7s %-8.8s %-24.24s %-24.24s %-4.4s\n";
    my %lun_maps = (); # List of LUN UUIDs we've assigned
    my $last_enc = -1;
    my $firstvol = 1; # For printing out the header stuff
    my @wwn_array; # Keep the order as they are used

    my $twig_root = $lunmgr_twig->root;

    my %DBnum = ();
    my %AUnum = ();

    log_msg("Distributing LUNs");

    if (%gh_logical_nodes)
    {
        SQVOLS: foreach my $sqvol (sort keys %gh_sqvolumes) # AU/DB/SYSTEM/TLOG
        {
            my $sqvol_name = $sqvol;
            $sqvol_name =~ s/\-.*// if ($config eq "shared");

            my ($prim_assigned, $mir_assigned) = (0, 0);

            # always search from the first enclosure for TLOG and SYSTEM
            $last_enc = -1 if ($sqvol =~ /(TLOG|SYSTEM)/);

            # Loop through the racks
            RACK: foreach my $rack ($twig_root->children)
            {
                my $racknum = $rack->att('number');

                next RACK if ($racknum != $gh_sqvolumes{$sqvol}{rack});

                # Only need to loop through the first switch, since all 
                # switches are referring to the same enclosures
                my @switches = $rack->children();
                my @encs = $switches[0]->children();

                # start from the next enclosure last visited, or from beginning
                # if we have reached the last one
                my $start_enc = ($last_enc < $#encs) ? $last_enc+1 : 0;

                DISKENC: for my $index ($start_enc .. $#encs)
                {
                    my $diskenc = $encs[$index];

                    my $wwn = $diskenc->att('wwpn'); # Get the WWN of this disk enclosure
                    $DBnum{$wwn} = 0 if (! defined $DBnum{$wwn});
                    $AUnum{$wwn} = 0 if (! defined $AUnum{$wwn});

                    next DISKENC if ($DBnum{$wwn} == DBMAX && $sqvol =~ /DB/);
                    next DISKENC if ($AUnum{$wwn} == AUMAX && $sqvol =~ /AU/);

                    LUN: foreach my $lun ($diskenc->children)
                    {
                        my ($device, ) = $lun->children_text;

                        # Use the first LUN that isn't marked as "used"
                        next LUN if ($gh_luns_seen{$device} eq "used");

                        # These are LUNs used for Bright Computing 
                        # /home [60] and /cm/shared [61]
                        if ($lun->att('id') == 60 || $lun->att('id') == 61)
                        {
                            my $eltmnt = $lun->child(0, 'mount_pt');
                            my $eltname = $lun->child(0, 'name');

                            if ($lun->att('id') == 60) {
                                $eltmnt->set_text("/");
                                $eltname->set_text("home");
                            }
                            else {
                                $eltmnt->set_text("/cm");
                                $eltname->set_text("shared");
                            }

                            $gh_luns_seen{$device} = "used";
                                
                            next LUN;
                        }

                        # Only assign these LUNs to $TLOG or $SYSTEM
                        if ($sqvol =~ /(TLOG|SYSTEM)/)
                        {
                            next LUN if ($lun->att('id') != 50 
                                        && $lun->att('id') != 51);
                        }
                        else
                        {
                            next LUN if ($lun->att('id') == 50 
                                       || $lun->att('id') == 51);
                        }

                        # pub AU volumes just the first of each RAID5 array
                        next LUN if ($sqvol =~ /AU/ && ($lun->att('id') % 5) != 0);
                        next LUN if ($sqvol =~ /DB/ && ($lun->att('id') % 5) == 0);

                        my $eltmnt = $lun->child(0, 'mount_pt');
                        my $eltname = $lun->child(0, 'name');

                        $eltname->set_text($sqvol_name);
                        $gh_luns_seen{$device} = "used";

                        $DBnum{$wwn}++ if ($sqvol =~ /DB/);
                        $AUnum{$wwn}++ if ($sqvol =~ /AU/);

                        $last_enc = $index if ($sqvol !~ /(TLOG|SYSTEM)/);

                        if ($prim_assigned == 0 && $mir_assigned == 0)
                        {
                            $eltmnt->set_text($gh_sqvolumes{$sqvol}{'prim-loc'});
                            $prim_assigned = 1;
                            $gh_sqvolumes{$sqvol}{'prim-wwn'} = $wwn;
                            $gh_sqvolumes{$sqvol}{'prim-lun'} = $lun->att('id');
                            $lun_maps{$wwn}{$lun->att('id')} = $gh_sqvolumes{$sqvol}{'node-name'};

                            next DISKENC;
                        }
                        elsif ($prim_assigned == 1 && $mir_assigned == 0)
                        {
                            $eltmnt->set_text($gh_sqvolumes{$sqvol}{'mir-loc'});
                            $mir_assigned = 1;
                            $gh_sqvolumes{$sqvol}{'mir-wwn'} = $wwn;
                            $gh_sqvolumes{$sqvol}{'mir-lun'} = $lun->att('id');
                            $lun_maps{$wwn}{$lun->att('id')} = $gh_sqvolumes{$sqvol}{'node-name'};

                            last RACK; # Go to print this SQ volume info and then to the next volume
                        }
                    }
                    # If we get here, it means this disk enclosure has no free LUNs
                } # End DISKENC loop
            } # End RACK loop

            # If we get here and one of the PRIM or MIR hasn't been assigned,
            # we've most likely run out of LUNs
            logerr_n_die("Error assigning $sqvol to a LUN; processing cannot continue!\nIt is likely that the sqconfig specifies more volumes than there are LUNs available") if ($prim_assigned == 0 || $mir_assigned == 0);

            if ($firstvol)
            {
                print "\n" . scalar(keys(%gh_sqvolumes)) . " SeaQuest volumes configured:\n\n";
                printf($OUT_FMT, "Volname", "LogNode", "PhysNode", "Primary", "Mirror", "Rack");
                printf($OUT_FMT, DASHES, DASHES, DASHES, DASHES, DASHES, DASHES);

                $firstvol = 0; # Reset so we don't print this again
            }

            print sprintf($OUT_FMT,
                $sqvol,
                $gh_sqvolumes{$sqvol}{'node-id'},
                $gh_sqvolumes{$sqvol}{'node-name'},
                $gh_sqvolumes{$sqvol}{'prim-wwn'} . ": LUN " . $gh_sqvolumes{$sqvol}{'prim-lun'},
                $gh_sqvolumes{$sqvol}{'mir-wwn'} . ": LUN " . $gh_sqvolumes{$sqvol}{'mir-lun'},
                $gh_sqvolumes{$sqvol}{'rack'}
            )
        } # End SQVOLS loop

        log_msg("Outputting disk enclosure/node map to $csvfile");

        sysopen(CSVFILE, $csvfile, O_CREAT|O_TRUNC|O_RDWR) ||
            logerr_n_die("Error opening CSV file $csvfile: $!");

        # Write out a CSV file for easy viewing in Excel
        foreach my $enc (keys(%lun_maps))
        {
            print CSVFILE "$enc\n";

            foreach (sort({$a <=> $b} keys(%{$lun_maps{$enc}})))
            {
                print CSVFILE "," . $lun_maps{$enc}{$_};
            }

            print CSVFILE "\n";
        }

        close(CSVFILE);
    }

    # Distribute the free LUNs
    if ($gi_name_free_luns)
    {
        my ($racknum, $lunnum, $boxnum);

        foreach my $rack ($twig_root->children)
        {
            $racknum = $rack->att('number');

            foreach my $sw ($rack->children)
            {
                $boxnum = 0;

                foreach my $diskenc ($sw->children)
                {
                    $boxnum++;

                    LUN: foreach my $lun ($diskenc->children)
                    {
                        my ($device, ) = $lun->children_text;

                        next if ($gh_luns_seen{$device} eq "used");

                        my $eltmnt = $lun->child(0, 'mount_pt');
                        my $eltname = $lun->child(0, 'name');
                        $lunnum = $lun->att('id');

                        $eltname->set_text("R$racknum" . "B$boxnum" . "L$lunnum");
                        $eltmnt->set_text("/database");
                        $gh_luns_seen{$device} = "used";
                    }
                }
            }
        }
    }

    log_msg("Overwriting the original XML with the changes");
    $lunmgr_twig->print_to_file($lunmgr_config); # Overwrite the original XML with the changes

}

main();
